<!DOCTYPE html>

<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="zh-CN" lang="zh-CN">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1"/>

<title>Action Cable 概览 — Ruby on Rails Guides</title>
<link rel="stylesheet" type="text/css" href="stylesheets/style.css" />
<link rel="stylesheet" type="text/css" href="stylesheets/print.css" media="print" />

<link rel="stylesheet" type="text/css" href="stylesheets/syntaxhighlighter/shCore.css" />
<link rel="stylesheet" type="text/css" href="stylesheets/syntaxhighlighter/shThemeRailsGuides.css" />

<link rel="stylesheet" type="text/css" href="stylesheets/fixes.css" />

<link href="images/favicon.ico" rel="shortcut icon" type="image/x-icon" />
</head>
<body class="guide">
  <div id="topNav">
    <div class="wrapper">
      <strong class="more-info-label">更多内容 <a href="http://rubyonrails.org/">rubyonrails.org:</a> </strong>
      <span class="red-button more-info-button">
        更多内容
      </span>
      <ul class="more-info-links s-hidden">
        <li class="more-info"><a href="http://weblog.rubyonrails.org/">博客</a></li>
        <li class="more-info"><a href="http://guides.rubyonrails.org/">指南</a></li>
        <li class="more-info"><a href="http://api.rubyonrails.org/">API</a></li>
        <li class="more-info"><a href="http://stackoverflow.com/questions/tagged/ruby-on-rails">提问</a></li>
        <li class="more-info"><a href="https://github.com/rails/rails">到 GitHub 贡献</a></li>
        <li class="more-info"><a href="https://ruby-china.org/">Ruby China 社区</a></li>
      </ul>
    </div>
  </div>
  <div id="header">
    <div class="wrapper clearfix">
      <h1><a href="index.html" title="返回首页">Rails 指南</a></h1>
      <ul class="nav">
        <li><a class="nav-item" href="index.html">首页</a></li>
        <li class="guides-index guides-index-large">
          <a href="index.html" id="guidesMenu" class="guides-index-item nav-item">指南索引</a>
          <div id="guides" class="clearfix" style="display: none;">
            <hr />
              <dl class="L">
                <dt>新手入门</dt>
                <dd><a href="getting_started.html">Rails 入门</a></dd>
                <dt>模型</dt>
                <dd><a href="active_record_basics.html">Active Record 基础</a></dd>
                <dd><a href="active_record_migrations.html">Active Record 迁移</a></dd>
                <dd><a href="active_record_validations.html">Active Record 数据验证</a></dd>
                <dd><a href="active_record_callbacks.html">Active Record 回调</a></dd>
                <dd><a href="association_basics.html">Active Record 关联</a></dd>
                <dd><a href="active_record_querying.html">Active Record 查询接口</a></dd>
                <dt>视图</dt>
                <dd><a href="layouts_and_rendering.html">Rails 布局和视图渲染</a></dd>
                <dd><a href="form_helpers.html">Action View 表单辅助方法</a></dd>
                <dt>控制器</dt>
                <dd><a href="action_controller_overview.html">Action Controller 概览</a></dd>
                <dd><a href="routing.html">Rails 路由全解</a></dd>
              </dl>
              <dl class="R">
                <dt>深入探索</dt>
                <dd><a href="active_support_core_extensions.html">Active Support 核心扩展</a></dd>
                <dd><a href="i18n.html">Rails 国际化 API</a></dd>
                <dd><a href="action_mailer_basics.html">Action Mailer 基础</a></dd>
                <dd><a href="active_job_basics.html">Active Job 基础</a></dd>
                <dd><a href="testing.html">Rails 应用测试指南</a></dd>
                <dd><a href="security.html">Ruby on Rails 安全指南</a></dd>
                <dd><a href="debugging_rails_applications.html">调试 Rails 应用</a></dd>
                <dd><a href="configuring.html">配置 Rails 应用</a></dd>
                <dd><a href="command_line.html">Rails 命令行</a></dd>
                <dd><a href="asset_pipeline.html">Asset Pipeline</a></dd>
                <dd><a href="working_with_javascript_in_rails.html">在 Rails 中使用 JavaScript</a></dd>
                <dd><a href="autoloading_and_reloading_constants.html">自动加载和重新加载常量</a></dd>
                <dd><a href="caching_with_rails.html">Rails 缓存概览</a></dd>
                <dd><a href="api_app.html">使用 Rails 开发只提供 API 的应用</a></dd>
                <dd><a href="action_cable_overview.html">Action Cable 概览</a></dd>
                <dt>扩展 Rails</dt>
                <dd><a href="rails_on_rack.html">Rails on Rack</a></dd>
                <dd><a href="generators.html">创建及定制 Rails 生成器和模板</a></dd>
                <dt>为 Ruby on Rails 做贡献</dt>
                <dd><a href="contributing_to_ruby_on_rails.html">为 Ruby on Rails 做贡献</a></dd>
                <dd><a href="api_documentation_guidelines.html">API 文档指导方针</a></dd>
                <dd><a href="ruby_on_rails_guides_guidelines.html">Ruby on Rails 指南指导方针</a></dd>
                <dt>维护方针</dt>
                <dd><a href="maintenance_policy.html">Ruby on Rails 的维护方针</a></dd>
                <dt>发布记</dt>
                <dd><a href="upgrading_ruby_on_rails.html">Ruby on Rails 升级指南</a></dd>
                <dd><a href="5_0_release_notes.html">Ruby on Rails 5.0 发布记</a></dd>
                <dd><a href="4_2_release_notes.html">Ruby on Rails 4.2 发布记</a></dd>
                <dd><a href="4_1_release_notes.html">Ruby on Rails 4.1 发布记</a></dd>
                <dd><a href="4_0_release_notes.html">Ruby on Rails 4.0 发布记</a></dd>
                <dd><a href="3_2_release_notes.html">Ruby on Rails 3.2 发布记</a></dd>
                <dd><a href="3_1_release_notes.html">Ruby on Rails 3.1 发布记</a></dd>
                <dd><a href="3_0_release_notes.html">Ruby on Rails 3.0 发布记</a></dd>
                <dd><a href="2_3_release_notes.html">Ruby on Rails 2.3 发布记</a></dd>
                <dd><a href="2_2_release_notes.html">Ruby on Rails 2.2 发布记</a></dd>
              </dl>
          </div>
        </li>
        <li><a class="nav-item" href="contributing_to_ruby_on_rails.html">贡献</a></li>
        <li><a class="nav-item" href="credits.html">感谢</a></li>
        <li class="guides-index guides-index-small">
          <select class="guides-index-item nav-item">
            <option value="index.html">指南索引</option>
              <optgroup label="新手入门">
                  <option value="getting_started.html">Rails 入门</option>
              </optgroup>
              <optgroup label="模型">
                  <option value="active_record_basics.html">Active Record 基础</option>
                  <option value="active_record_migrations.html">Active Record 迁移</option>
                  <option value="active_record_validations.html">Active Record 数据验证</option>
                  <option value="active_record_callbacks.html">Active Record 回调</option>
                  <option value="association_basics.html">Active Record 关联</option>
                  <option value="active_record_querying.html">Active Record 查询接口</option>
              </optgroup>
              <optgroup label="视图">
                  <option value="layouts_and_rendering.html">Rails 布局和视图渲染</option>
                  <option value="form_helpers.html">Action View 表单辅助方法</option>
              </optgroup>
              <optgroup label="控制器">
                  <option value="action_controller_overview.html">Action Controller 概览</option>
                  <option value="routing.html">Rails 路由全解</option>
              </optgroup>
              <optgroup label="深入探索">
                  <option value="active_support_core_extensions.html">Active Support 核心扩展</option>
                  <option value="i18n.html">Rails 国际化 API</option>
                  <option value="action_mailer_basics.html">Action Mailer 基础</option>
                  <option value="active_job_basics.html">Active Job 基础</option>
                  <option value="testing.html">Rails 应用测试指南</option>
                  <option value="security.html">Ruby on Rails 安全指南</option>
                  <option value="debugging_rails_applications.html">调试 Rails 应用</option>
                  <option value="configuring.html">配置 Rails 应用</option>
                  <option value="command_line.html">Rails 命令行</option>
                  <option value="asset_pipeline.html">Asset Pipeline</option>
                  <option value="working_with_javascript_in_rails.html">在 Rails 中使用 JavaScript</option>
                  <option value="autoloading_and_reloading_constants.html">自动加载和重新加载常量</option>
                  <option value="caching_with_rails.html">Rails 缓存概览</option>
                  <option value="api_app.html">使用 Rails 开发只提供 API 的应用</option>
                  <option value="action_cable_overview.html">Action Cable 概览</option>
              </optgroup>
              <optgroup label="扩展 Rails">
                  <option value="rails_on_rack.html">Rails on Rack</option>
                  <option value="generators.html">创建及定制 Rails 生成器和模板</option>
              </optgroup>
              <optgroup label="为 Ruby on Rails 做贡献">
                  <option value="contributing_to_ruby_on_rails.html">为 Ruby on Rails 做贡献</option>
                  <option value="api_documentation_guidelines.html">API 文档指导方针</option>
                  <option value="ruby_on_rails_guides_guidelines.html">Ruby on Rails 指南指导方针</option>
              </optgroup>
              <optgroup label="维护方针">
                  <option value="maintenance_policy.html">Ruby on Rails 的维护方针</option>
              </optgroup>
              <optgroup label="发布记">
                  <option value="upgrading_ruby_on_rails.html">Ruby on Rails 升级指南</option>
                  <option value="5_0_release_notes.html">Ruby on Rails 5.0 发布记</option>
                  <option value="4_2_release_notes.html">Ruby on Rails 4.2 发布记</option>
                  <option value="4_1_release_notes.html">Ruby on Rails 4.1 发布记</option>
                  <option value="4_0_release_notes.html">Ruby on Rails 4.0 发布记</option>
                  <option value="3_2_release_notes.html">Ruby on Rails 3.2 发布记</option>
                  <option value="3_1_release_notes.html">Ruby on Rails 3.1 发布记</option>
                  <option value="3_0_release_notes.html">Ruby on Rails 3.0 发布记</option>
                  <option value="2_3_release_notes.html">Ruby on Rails 2.3 发布记</option>
                  <option value="2_2_release_notes.html">Ruby on Rails 2.2 发布记</option>
              </optgroup>
          </select>
        </li>
      </ul>
    </div>
  </div>
  <hr class="hide" />

  <div id="feature">
    <div class="wrapper">
      <h2>Action Cable 概览</h2><p>本文介绍 Action Cable 的工作原理，以及在 Rails 应用中如何通过 WebSocket 实现实时功能。</p><p>读完本文后，您将学到：</p>
<ul>
<li>  Action Cable 是什么，以及对前后端的集成；</li>
<li>  如何设置 Action Cable；</li>
<li>  如何设置频道（channel）；</li>
<li>  Action Cable 的部署和架构设置。</li>
</ul>


              <div id="subCol">
          <h3 class="chapter"><img src="images/chapters_icon.gif" alt="" />目录</h3>
          <ol class="chapters">
<li><a href="#introduction">简介</a></li>
<li><a href="#what-is-pub-sub">Pub/Sub 是什么</a></li>
<li>
<a href="#server-side-components">服务器端组件</a>

<ul>
<li><a href="#server-side-connections">连接</a></li>
<li><a href="#channels">频道</a></li>
</ul>
</li>
<li>
<a href="#client-side-components">客户端组件</a>

<ul>
<li><a href="#client-side-connections">连接</a></li>
</ul>
</li>
<li>
<a href="#client-server-interactions">客户端-服务器的交互</a>

<ul>
<li><a href="#streams">流（stream）</a></li>
<li><a href="#broadcasting">广播</a></li>
<li><a href="#client-server-interactions-subscriptions">订阅</a></li>
<li><a href="#passing-parameters-to-channels">向频道传递参数</a></li>
<li><a href="#rebroadcasting-a-message">消息重播</a></li>
</ul>
</li>
<li>
<a href="#full-stack-examples">全栈示例</a>

<ul>
<li><a href="#example-one-user-appearances">例 1：用户在线状态（user appearance）</a></li>
<li><a href="#example-two-receiving-new-web-notifications">例 2：接收新的 Web 通知</a></li>
<li><a href="#more-complete-examples">更完整的例子</a></li>
</ul>
</li>
<li>
<a href="#configuration">配置</a>

<ul>
<li><a href="#subscription-adapter">订阅适配器</a></li>
<li><a href="#allowed-request-origins">允许的请求来源</a></li>
<li><a href="#consumer-configuration">用户配置</a></li>
<li><a href="#other-configurations">其他配置</a></li>
</ul>
</li>
<li>
<a href="#running-standalone-cable-servers">运行独立的 Cable 服务器</a>

<ul>
<li><a href="#in-app">和应用一起运行</a></li>
<li><a href="#standalone">独立运行</a></li>
<li><a href="#notes">注意事项</a></li>
</ul>
</li>
<li><a href="#action-cable-overview-dependencies">依赖关系</a></li>
<li><a href="#deployment">部署</a></li>
</ol>

        </div>

    </div>
  </div>

  <div id="container">
    <div class="wrapper">
      <div id="mainCol">
        <p><a class="anchor" id="introduction"></a></p><h3 id="introduction">1 简介</h3><p>Action Cable 将 <a href="https://en.wikipedia.org/wiki/WebSocket">WebSocket</a> 与 Rails 应用的其余部分无缝集成。有了 Action Cable，我们就可以用 Ruby 语言，以 Rails 风格实现实时功能，并且保持高性能和可扩展性。Action Cable 为此提供了全栈支持，包括客户端 JavaScript 框架和服务器端 Ruby 框架。同时，我们也能够通过 Action Cable 访问使用 Active Record 或其他 ORM 编写的所有模型。</p><p><a class="anchor" id="what-is-pub-sub"></a></p><h3 id="what-is-pub-sub">2 Pub/Sub 是什么</h3><p><a href="https://en.wikipedia.org/wiki/Publish%E2%80%93subscribe_pattern">Pub/Sub</a>，也就是发布/订阅，是指在消息队列中，信息发送者（发布者）把数据发送给某一类接收者（订阅者），而不必单独指定接收者。Action Cable 通过发布/订阅的方式在服务器和多个客户端之间通信。</p><p><a class="anchor" id="server-side-components"></a></p><h3 id="server-side-components">3 服务器端组件</h3><p><a class="anchor" id="server-side-connections"></a></p><h4 id="server-side-connections">3.1 连接</h4><p>连接是客户端-服务器通信的基础。每当服务器接受一个 WebSocket，就会实例化一个连接对象。所有频道订阅（channel subscription）都是在继承连接对象的基础上创建的。连接本身并不处理身份验证和授权之外的任何应用逻辑。WebSocket 连接的客户端被称为连接用户（connection consumer）。每当用户新打开一个浏览器标签、窗口或设备，对应地都会新建一个用户-连接对（consumer-connection pair）。</p><p>连接是 <code>ApplicationCable::Connection</code> 类的实例。对连接的授权就是在这个类中完成的，对于能够识别的用户，才会继续建立连接。</p><p><a class="anchor" id="connection-setup"></a></p><h5 id="connection-setup">3.1.1 连接设置</h5><div class="code_container">
<pre class="brush: ruby; gutter: false; toolbar: false">
# app/channels/application_cable/connection.rb
module ApplicationCable
  class Connection &lt; ActionCable::Connection::Base
    identified_by :current_user

    def connect
      self.current_user = find_verified_user
    end

    private
      def find_verified_user
        if current_user = User.find_by(id: cookies.signed[:user_id])
          current_user
        else
          reject_unauthorized_connection
        end
      end
  end
end

</pre>
</div>
<p>其中 <code>identified_by</code> 用于声明连接标识符，连接标识符稍后将用于查找指定连接。注意，在声明连接标识符的同时，在基于连接创建的频道实例上，会自动创建同名委托（delegate）。</p><p>上述例子假设我们已经在应用的其他部分完成了用户身份验证，并且在验证成功后设置了经过用户 ID 签名的 cookie。</p><p>尝试建立新连接时，会自动把 cookie 发送给连接实例，用于设置 <code>current_user</code>。通过使用 <code>current_user</code> 标识连接，我们稍后就能够检索指定用户打开的所有连接（如果删除用户或取消对用户的授权，该用户打开的所有连接都会断开）。</p><p><a class="anchor" id="channels"></a></p><h4 id="channels">3.2 频道</h4><p>和常规 MVC 中的控制器类似，频道用于封装逻辑工作单元。默认情况下，Rails 会把 <code>ApplicationCable::Channel</code> 类作为频道的父类，用于封装频道之间共享的逻辑。</p><p><a class="anchor" id="parent-channel-setup"></a></p><h5 id="parent-channel-setup">3.2.1 父频道设置</h5><div class="code_container">
<pre class="brush: ruby; gutter: false; toolbar: false">
# app/channels/application_cable/channel.rb
module ApplicationCable
  class Channel &lt; ActionCable::Channel::Base
  end
end

</pre>
</div>
<p>接下来我们要创建自己的频道类。例如，可以创建 <code>ChatChannel</code> 和 <code>AppearanceChannel</code> 类：</p><div class="code_container">
<pre class="brush: ruby; gutter: false; toolbar: false">
# app/channels/chat_channel.rb
class ChatChannel &lt; ApplicationCable::Channel
end

# app/channels/appearance_channel.rb
class AppearanceChannel &lt; ApplicationCable::Channel
end

</pre>
</div>
<p>这样用户就可以订阅频道了，订阅一个或两个都行。</p><p><a class="anchor" id="subscriptions"></a></p><h5 id="subscriptions">3.2.2 订阅</h5><p>订阅频道的用户称为订阅者。用户创建的连接称为（频道）订阅。订阅基于连接用户（订阅者）发送的标识符创建，生成的消息将发送到这些订阅。</p><div class="code_container">
<pre class="brush: ruby; gutter: false; toolbar: false">
# app/channels/chat_channel.rb
class ChatChannel &lt; ApplicationCable::Channel
  # 当用户成为此频道的订阅者时调用
  def subscribed
  end
end

</pre>
</div>
<p><a class="anchor" id="client-side-components"></a></p><h3 id="client-side-components">4 客户端组件</h3><p><a class="anchor" id="client-side-connections"></a></p><h4 id="client-side-connections">4.1 连接</h4><p>用户需要在客户端创建连接实例。下面这段由 Rails 默认生成的 JavaScript 代码，正是用于在客户端创建连接实例：</p><p><a class="anchor" id="connect-consumer"></a></p><h5 id="connect-consumer">4.1.1 连接用户</h5><div class="code_container">
<pre class="brush: plain; gutter: false; toolbar: false">
// app/assets/javascripts/cable.js
//= require action_cable
//= require_self
//= require_tree ./channels

(function() {
  this.App || (this.App = {});

  App.cable = ActionCable.createConsumer();
}).call(this);

</pre>
</div>
<p>上述代码会创建连接用户，并将通过默认的 <code>/cable</code> 地址和服务器建立连接。我们还需要从现有订阅中至少选择一个感兴趣的订阅，否则将无法建立连接。</p><p><a class="anchor" id="subscriber"></a></p><h5 id="subscriber">4.1.2 订阅者</h5><p>一旦订阅了某个频道，用户也就成为了订阅者：</p><div class="code_container">
<pre class="brush: ruby; gutter: false; toolbar: false">
# app/assets/javascripts/cable/subscriptions/chat.coffee
App.cable.subscriptions.create { channel: "ChatChannel", room: "Best Room" }

# app/assets/javascripts/cable/subscriptions/appearance.coffee
App.cable.subscriptions.create { channel: "AppearanceChannel" }

</pre>
</div>
<p>上述代码创建了订阅，稍后我们还要描述如何处理接收到的数据。</p><p>作为订阅者，用户可以多次订阅同一个频道。例如，用户可以同时订阅多个聊天室：</p><div class="code_container">
<pre class="brush: ruby; gutter: false; toolbar: false">
App.cable.subscriptions.create { channel: "ChatChannel", room: "1st Room" }
App.cable.subscriptions.create { channel: "ChatChannel", room: "2nd Room" }

</pre>
</div>
<p><a class="anchor" id="client-server-interactions"></a></p><h3 id="client-server-interactions">5 客户端-服务器的交互</h3><p><a class="anchor" id="streams"></a></p><h4 id="streams">5.1 流（stream）</h4><p>频道把已发布内容（即广播）发送给订阅者，是通过所谓的“流”机制实现的。</p><div class="code_container">
<pre class="brush: ruby; gutter: false; toolbar: false">
# app/channels/chat_channel.rb
class ChatChannel &lt; ApplicationCable::Channel
  def subscribed
    stream_from "chat_#{params[:room]}"
  end
end

</pre>
</div>
<p>有了和模型关联的流，就可以从模型和频道生成所需的广播。下面的例子用于订阅评论频道，以接收 <code>Z2lkOi8vVGVzdEFwcC9Qb3N0LzE</code> 这样的广播：</p><div class="code_container">
<pre class="brush: ruby; gutter: false; toolbar: false">
class CommentsChannel &lt; ApplicationCable::Channel
  def subscribed
    post = Post.find(params[:id])
    stream_for post
  end
end

</pre>
</div>
<p>向评论频道发送广播的方式如下：</p><div class="code_container">
<pre class="brush: ruby; gutter: false; toolbar: false">
CommentsChannel.broadcast_to(@post, @comment)

</pre>
</div>
<p><a class="anchor" id="broadcasting"></a></p><h4 id="broadcasting">5.2 广播</h4><p>广播是指发布/订阅的链接，也就是说，当频道订阅者使用流接收某个广播时，发布者发布的内容会被直接发送给订阅者。</p><p>广播也是时间相关的在线队列。如果用户未使用流（即未订阅频道），稍后就无法接收到广播。</p><p>在 Rails 应用的其他部分也可以发送广播：</p><div class="code_container">
<pre class="brush: ruby; gutter: false; toolbar: false">
WebNotificationsChannel.broadcast_to(
  current_user,
  title: 'New things!',
  body: 'All the news fit to print'
)

</pre>
</div>
<p>调用 <code>WebNotificationsChannel.broadcast_to</code> 将向当前订阅适配器（生产环境默认为 <code>redis</code>，开发和测试环境默认为 <code>async</code>）的发布/订阅队列推送一条消息，并为每个用户设置不同的广播名。对于 ID 为 1 的用户，广播名是 <code>web_notifications:1</code>。</p><p>通过调用 <code>received</code> 回调方法，频道会使用流把到达 <code>web_notifications:1</code> 的消息直接发送给客户端。</p><p><a class="anchor" id="client-server-interactions-subscriptions"></a></p><h4 id="client-server-interactions-subscriptions">5.3 订阅</h4><p>订阅频道的用户，称为订阅者。用户创建的连接称为（频道）订阅。订阅基于连接用户（订阅者）发送的标识符创建，收到的消息将被发送到这些订阅。</p><div class="code_container">
<pre class="brush: plain; gutter: false; toolbar: false">
# app/assets/javascripts/cable/subscriptions/chat.coffee
# 假设我们已经获得了发送 Web 通知的权限
App.cable.subscriptions.create { channel: "ChatChannel", room: "Best Room" },
  received: (data) -&gt;
    @appendLine(data)

  appendLine: (data) -&gt;
    html = @createLine(data)
    $("[data-chat-room='Best Room']").append(html)

  createLine: (data) -&gt;
    """
    &lt;article class="chat-line"&gt;
      &lt;span class="speaker"&gt;#{data["sent_by"]}&lt;/span&gt;
      &lt;span class="body"&gt;#{data["body"]}&lt;/span&gt;
    &lt;/article&gt;
    """

</pre>
</div>
<p><a class="anchor" id="passing-parameters-to-channels"></a></p><h4 id="passing-parameters-to-channels">5.4 向频道传递参数</h4><p>创建订阅时，可以从客户端向服务器端传递参数。例如：</p><div class="code_container">
<pre class="brush: ruby; gutter: false; toolbar: false">
# app/channels/chat_channel.rb
class ChatChannel &lt; ApplicationCable::Channel
  def subscribed
    stream_from "chat_#{params[:room]}"
  end
end

</pre>
</div>
<p>传递给 <code>subscriptions.create</code> 方法并作为第一个参数的对象，将成为频道的参数散列。其中必需包含 <code>channel</code> 关键字：</p><div class="code_container">
<pre class="brush: plain; gutter: false; toolbar: false">
# app/assets/javascripts/cable/subscriptions/chat.coffee
App.cable.subscriptions.create { channel: "ChatChannel", room: "Best Room" },
  received: (data) -&gt;
    @appendLine(data)

  appendLine: (data) -&gt;
    html = @createLine(data)
    $("[data-chat-room='Best Room']").append(html)

  createLine: (data) -&gt;
    """
    &lt;article class="chat-line"&gt;
      &lt;span class="speaker"&gt;#{data["sent_by"]}&lt;/span&gt;
      &lt;span class="body"&gt;#{data["body"]}&lt;/span&gt;
    &lt;/article&gt;
    """

</pre>
</div>
<div class="code_container">
<pre class="brush: ruby; gutter: false; toolbar: false">
# 在应用的某个部分中调用，例如 NewCommentJob
ActionCable.server.broadcast(
  "chat_#{room}",
  sent_by: 'Paul',
  body: 'This is a cool chat app.'
)

</pre>
</div>
<p><a class="anchor" id="rebroadcasting-a-message"></a></p><h4 id="rebroadcasting-a-message">5.5 消息重播</h4><p>一个客户端向其他已连接客户端重播自己收到的消息，是一种常见用法。</p><div class="code_container">
<pre class="brush: ruby; gutter: false; toolbar: false">
# app/channels/chat_channel.rb
class ChatChannel &lt; ApplicationCable::Channel
  def subscribed
    stream_from "chat_#{params[:room]}"
  end

  def receive(data)
    ActionCable.server.broadcast("chat_#{params[:room]}", data)
  end
end

</pre>
</div>
<div class="code_container">
<pre class="brush: plain; gutter: false; toolbar: false">
# app/assets/javascripts/cable/subscriptions/chat.coffee
App.chatChannel = App.cable.subscriptions.create { channel: "ChatChannel", room: "Best Room" },
  received: (data) -&gt;
    # data =&gt; { sent_by: "Paul", body: "This is a cool chat app." }

App.chatChannel.send({ sent_by: "Paul", body: "This is a cool chat app." })

</pre>
</div>
<p>所有已连接的客户端，包括发送消息的客户端在内，都将收到重播的消息。注意，重播时使用的参数与订阅频道时使用的参数相同。</p><p><a class="anchor" id="full-stack-examples"></a></p><h3 id="full-stack-examples">6 全栈示例</h3><p>本节的两个例子都需要进行下列设置：</p>
<ol>
<li> 设置连接；</li>
<li> 设置父频道；</li>
<li> 连接用户。</li>
</ol>
<p><a class="anchor" id="example-one-user-appearances"></a></p><h4 id="example-one-user-appearances">6.1 例 1：用户在线状态（user appearance）</h4><p>下面是一个关于频道的简单例子，用于跟踪用户是否在线，以及用户所在的页面。（常用于显示用户在线状态，例如当用户在线时，在用户名旁边显示绿色小圆点。）</p><p>在服务器端创建在线状态频道（appearance channel）：</p><div class="code_container">
<pre class="brush: ruby; gutter: false; toolbar: false">
# app/channels/appearance_channel.rb
class AppearanceChannel &lt; ApplicationCable::Channel
  def subscribed
    current_user.appear
  end

  def unsubscribed
    current_user.disappear
  end

  def appear(data)
    current_user.appear(on: data['appearing_on'])
  end

  def away
    current_user.away
  end
end

</pre>
</div>
<p>订阅创建后，会触发 <code>subscribed</code> 回调方法，这时可以提示说“当前用户上线了”。上线/下线 API 的后端可以是 Redis、数据库或其他解决方案。</p><p>在客户端创建在线状态频道订阅：</p><div class="code_container">
<pre class="brush: plain; gutter: false; toolbar: false">
# app/assets/javascripts/cable/subscriptions/appearance.coffee
App.cable.subscriptions.create "AppearanceChannel",
  # 当服务器上的订阅可用时调用
  connected: -&gt;
    @install()
    @appear()

  # 当 WebSocket 连接关闭时调用
  disconnected: -&gt;
    @uninstall()

  # 当服务器拒绝订阅时调用
  rejected: -&gt;
    @uninstall()

  appear: -&gt;
    # 在服务器上调用 `AppearanceChannel#appear(data)`
    @perform("appear", appearing_on: $("main").data("appearing-on"))

  away: -&gt;
    # 在服务器上调用 `AppearanceChannel#away`
    @perform("away")


  buttonSelector = "[data-behavior~=appear_away]"

  install: -&gt;
    $(document).on "turbolinks:load.appearance", =&gt;
      @appear()

    $(document).on "click.appearance", buttonSelector, =&gt;
      @away()
      false

    $(buttonSelector).show()

  uninstall: -&gt;
    $(document).off(".appearance")
    $(buttonSelector).hide()

</pre>
</div>
<p><a class="anchor" id="client-server-interaction"></a></p><h5 id="client-server-interaction">6.1.1 客户端-服务器交互</h5>
<ol>
<li> <strong>客户端</strong>通过 <code>App.cable = ActionCable.createConsumer("ws://cable.example.com")</code>（位于 <code>cable.js</code> 文件中）连接到<strong>服务器</strong>。<strong>服务器</strong>通过 <code>current_user</code> 标识此连接。</li>
<li> <strong>客户端</strong>通过 <code>App.cable.subscriptions.create(channel: "AppearanceChannel")</code>（位于 <code>appearance.coffee</code> 文件中）订阅在线状态频道。</li>
<li> <strong>服务器</strong>发现在线状态频道创建了一个新订阅，于是调用 <code>subscribed</code> 回调方法，也即在 <code>current_user</code> 对象上调用 <code>appear</code> 方法。</li>
<li> <strong>客户端</strong>发现订阅创建成功，于是调用 <code>connected</code> 方法（位于 <code>appearance.coffee</code> 文件中），也即依次调用 <code>@install</code> 和 <code>@appear</code>。<code>@appear</code> 会调用服务器上的 <code>AppearanceChannel#appear(data)</code> 方法，同时提供 <code>{ appearing_on: $("main").data("appearing-on") }</code> 数据散列。之所以能够这样做，是因为服务器端的频道实例会自动暴露类上声明的所有公共方法（回调除外），从而使远程过程能够通过订阅的 <code>perform</code> 方法调用它们。</li>
<li> <strong>服务器</strong>接收向在线状态频道的 <code>appear</code> 动作发起的请求，此频道基于连接创建，连接由 <code>current_user</code>（位于 <code>appearance_channel.rb</code> 文件中）标识。<strong>服务器</strong>通过 <code>:appearing_on</code> 键从数据散列中检索数据，将其设置为 <code>:on</code> 键的值并传递给 <code>current_user.appear</code>。</li>
</ol>
<p><a class="anchor" id="example-two-receiving-new-web-notifications"></a></p><h4 id="example-two-receiving-new-web-notifications">6.2 例 2：接收新的 Web 通知</h4><p>上一节中在线状态的例子，演示了如何把服务器功能暴露给客户端，以便在客户端通过 WebSocket 连接调用这些功能。但是 WebSocket 的伟大之处在于，它是一条双向通道。因此，在本节的例子中，我们要看一看服务器如何调用客户端上的动作。</p><p>本节所举的例子是一个 Web 通知频道（Web notification channel），允许我们在广播到正确的流时触发客户端 Web 通知。</p><p>创建服务器端 Web 通知频道：</p><div class="code_container">
<pre class="brush: ruby; gutter: false; toolbar: false">
# app/channels/web_notifications_channel.rb
class WebNotificationsChannel &lt; ApplicationCable::Channel
  def subscribed
    stream_for current_user
  end
end

</pre>
</div>
<p>创建客户端 Web 通知频道订阅：</p><div class="code_container">
<pre class="brush: plain; gutter: false; toolbar: false">
# app/assets/javascripts/cable/subscriptions/web_notifications.coffee
# 客户端假设我们已经获得了发送 Web 通知的权限
App.cable.subscriptions.create "WebNotificationsChannel",
  received: (data) -&gt;
    new Notification data["title"], body: data["body"]

</pre>
</div>
<p>在应用的其他部分向 Web 通知频道实例发送内容广播：</p><div class="code_container">
<pre class="brush: ruby; gutter: false; toolbar: false">
# 在应用的某个部分中调用，例如 NewCommentJob
WebNotificationsChannel.broadcast_to(
  current_user,
  title: 'New things!',
  body: 'All the news fit to print'
)

</pre>
</div>
<p>调用 <code>WebNotificationsChannel.broadcast_to</code> 将向当前订阅适配器的发布/订阅队列推送一条消息，并为每个用户设置不同的广播名。对于 ID 为 1 的用户，广播名是 <code>web_notifications:1</code>。</p><p>通过调用 <code>received</code> 回调方法，频道会用流把到达 <code>web_notifications:1</code> 的消息直接发送给客户端。作为参数传递的数据散列，将作为第二个参数传递给服务器端的广播调用，数据在传输前使用 JSON 进行编码，到达服务器后由 <code>received</code> 解码。</p><p><a class="anchor" id="more-complete-examples"></a></p><h4 id="more-complete-examples">6.3 更完整的例子</h4><p>关于在 Rails 应用中设置 Action Cable 并添加频道的完整例子，参见 <a href="https://github.com/rails/actioncable-examples">rails/actioncable-examples</a> 仓库。</p><p><a class="anchor" id="configuration"></a></p><h3 id="configuration">7 配置</h3><p>使用 Action Cable 时，有两个选项必需配置：订阅适配器和允许的请求来源。</p><p><a class="anchor" id="subscription-adapter"></a></p><h4 id="subscription-adapter">7.1 订阅适配器</h4><p>默认情况下，Action Cable 会查找 <code>config/cable.yml</code> 这个配置文件。该文件必须为每个 Rails 环境指定适配器和 URL 地址。关于适配器的更多介绍，请参阅 <a href="#action-cable-overview-dependencies">依赖关系</a>。</p><div class="code_container">
<pre class="brush: plain; gutter: false; toolbar: false">
development:
  adapter: async

test:
  adapter: async

production:
  adapter: redis
  url: redis://10.10.3.153:6381
  channel_prefix: appname_production

</pre>
</div>
<p><a class="anchor" id="adapter-configuration"></a></p><h5 id="adapter-configuration">7.1.1 配置适配器</h5><p>下面是终端用户可用的订阅适配器。</p><p><a class="anchor" id="async-adapter"></a></p><h6 id="async-adapter">7.1.1.1 async 适配器</h6><p>async 适配器只适用于开发和测试环境，不应该在生产环境使用。</p><p><a class="anchor" id="redis-adapter"></a></p><h6 id="redis-adapter">7.1.1.2 Redis 适配器</h6><p>Action Cable 包含两个 Redis 适配器：常规的 Redis 和事件型 Redis。这两个适配器都要求用户提供指向 Redis 服务器的 URL。此外，多个应用使用同一个 Redis 服务器时，可以设定 <code>channel_prefix</code>，以免名称冲突。详情参见 <a href="https://redis.io/topics/pubsub#database-amp-scoping">Redis PubSub 文档</a>。</p><p><a class="anchor" id="postgresql-adapter"></a></p><h6 id="postgresql-adapter">7.1.1.3 PostgreSQL 适配器</h6><p>PostgreSQL 适配器使用 Active Record 的连接池，因此使用应用的 <code>config/database.yml</code> 数据库配置连接。以后可能会变。<a href="https://github.com/rails/rails/issues/27214">#27214</a></p><p><a class="anchor" id="allowed-request-origins"></a></p><h4 id="allowed-request-origins">7.2 允许的请求来源</h4><p>Action Cable 仅接受来自指定来源的请求。这些来源是在服务器配置文件中以数组的形式设置的，每个来源既可以是字符串，也可以是正则表达式。对于每个请求，都要对其来源进行检查，看是否和允许的请求来源相匹配。</p><div class="code_container">
<pre class="brush: ruby; gutter: false; toolbar: false">
config.action_cable.allowed_request_origins = ['http://rubyonrails.com', %r{http://ruby.*}]

</pre>
</div>
<p>若想禁用来源检查，允许任何来源的请求：</p><div class="code_container">
<pre class="brush: ruby; gutter: false; toolbar: false">
config.action_cable.disable_request_forgery_protection = true

</pre>
</div>
<p>在开发环境中，Action Cable 默认允许来自 localhost:3000 的所有请求。</p><p><a class="anchor" id="consumer-configuration"></a></p><h4 id="consumer-configuration">7.3 用户配置</h4><p>要想配置 URL 地址，可以在 HTML 布局文件的 <code>&lt;head&gt;</code> 元素中添加 <code>action_cable_meta_tag</code> 标签。这个标签会使用环境配置文件中 <code>config.action_cable.url</code> 选项设置的 URL 地址或路径。</p><p><a class="anchor" id="other-configurations"></a></p><h4 id="other-configurations">7.4 其他配置</h4><p>另一个常见的配置选项，是应用于每个连接记录器的日志标签。下述示例在有用户账户时使用账户 ID，没有时则标记为“no-account”：</p><div class="code_container">
<pre class="brush: ruby; gutter: false; toolbar: false">
config.action_cable.log_tags = [
  -&gt; request { request.env['user_account_id'] || "no-account" },
  :action_cable,
  -&gt; request { request.uuid }
]

</pre>
</div>
<p>关于所有配置选项的完整列表，请参阅 <code>ActionCable::Server::Configuration</code> 类的 API 文档。</p><p>还要注意，服务器提供的数据库连接在数量上至少应该和职程（worker）相等。职程池的默认大小为 100，也就是说数据库连接数量至少为 4。职程池的大小可以通过 <code>config/database.yml</code> 文件中的 <code>pool</code> 属性设置。</p><p><a class="anchor" id="running-standalone-cable-servers"></a></p><h3 id="running-standalone-cable-servers">8 运行独立的 Cable 服务器</h3><p><a class="anchor" id="in-app"></a></p><h4 id="in-app">8.1 和应用一起运行</h4><p>Action Cable 可以和 Rails 应用一起运行。例如，要想监听 <code>/websocket</code> 上的 WebSocket 请求，可以通过 <code>config.action_cable.mount_path</code> 选项指定监听路径：</p><div class="code_container">
<pre class="brush: ruby; gutter: false; toolbar: false">
# config/application.rb
class Application &lt; Rails::Application
  config.action_cable.mount_path = '/websocket'
end

</pre>
</div>
<p>在布局文件中调用 <code>action_cable_meta_tag</code> 后，就可以使用 <code>App.cable = ActionCable.createConsumer()</code> 连接到 Cable 服务器。可以通过 <code>createConsumer</code> 方法的第一个参数指定自定义路径（例如，<code>App.cable =
ActionCable.createConsumer("/websocket")</code>）。</p><p>对于我们创建的每个服务器实例，以及由服务器派生的每个职程，都会新建对应的 Action Cable 实例，通过 Redis 可以在不同连接之间保持消息同步。</p><p><a class="anchor" id="standalone"></a></p><h4 id="standalone">8.2 独立运行</h4><p>Cable 服务器可以和普通应用服务器分离。此时，Cable 服务器仍然是 Rack 应用，只不过是单独的 Rack 应用罢了。推荐的基本设置如下：</p><div class="code_container">
<pre class="brush: ruby; gutter: false; toolbar: false">
# cable/config.ru
require_relative '../config/environment'
Rails.application.eager_load!

run ActionCable.server

</pre>
</div>
<p>然后用 <code>bin/cable</code> 中的一个 binstub 命令启动服务器：</p><div class="code_container">
<pre class="brush: plain; gutter: false; toolbar: false">
#!/bin/bash
bundle exec puma -p 28080 cable/config.ru

</pre>
</div>
<p>上述代码在 28080 端口上启动 Cable 服务器。</p><p><a class="anchor" id="notes"></a></p><h4 id="notes">8.3 注意事项</h4><p>WebSocket 服务器没有访问会话的权限，但可以访问 cookie，而在处理身份验证时需要用到 cookie。<a href="http://www.rubytutorial.io/actioncable-devise-authentication">这篇文章</a>介绍了如何使用 Devise 验证身份。</p><p><a class="anchor" id="action-cable-overview-dependencies"></a></p><h3 id="action-cable-overview-dependencies">9 依赖关系</h3><p>Action Cable 提供了用于处理发布/订阅内部逻辑的订阅适配器接口，默认包含异步、内联、PostgreSQL、事件 Redis 和非事件 Redis 适配器。新建 Rails 应用的默认适配器是异步（async）适配器。</p><p>对 Ruby gem 的依赖包括 <a href="https://github.com/faye/websocket-driver-ruby">websocket-driver</a>、<a href="https://github.com/celluloid/nio4r">nio4r</a> 和 <a href="https://github.com/ruby-concurrency/concurrent-ruby">concurrent-ruby</a>。</p><p><a class="anchor" id="deployment"></a></p><h3 id="deployment">10 部署</h3><p>Action Cable 由 WebSocket 和线程组成。其中框架管道和用户指定频道的职程，都是通过 Ruby 提供的原生线程支持来处理的。这意味着，只要不涉及线程安全问题，我们就可以使用常规 Rails 线程模型的所有功能。</p><p>Action Cable 服务器实现了Rack 套接字劫持 API（Rack socket hijacking API），因此无论应用服务器是否是多线程的，都能够通过多线程模式管理内部连接。</p><p>因此，Action Cable 可以和流行的应用服务器一起使用，例如 Unicorn、Puma 和 Passenger。</p>

        <h3>反馈</h3>
        <p>
          我们鼓励您帮助提高本指南的质量。
        </p>
        <p>
          如果看到如何错字或错误，请反馈给我们。
          您可以阅读我们的<a href="http://edgeguides.rubyonrails.org/contributing_to_ruby_on_rails.html#contributing-to-the-rails-documentation">文档贡献</a>指南。
        </p>
        <p>
          您还可能会发现内容不完整或不是最新版本。
          请添加缺失文档到 master 分支。请先确认 <a href="http://edgeguides.rubyonrails.org">Edge Guides</a> 是否已经修复。
          关于用语约定，请查看<a href="ruby_on_rails_guides_guidelines.html">Ruby on Rails 指南指导</a>。
        </p>
        <p>
          无论什么原因，如果你发现了问题但无法修补它，请<a href="https://github.com/rails/rails/issues">创建 issue</a>。
        </p>
        <p>
          最后，欢迎到 <a href="http://groups.google.com/group/rubyonrails-docs">rubyonrails-docs 邮件列表</a>参与任何有关 Ruby on Rails 文档的讨论。
        </p>
        <h4>中文翻译反馈</h4>
        <p>贡献：<a href="https://github.com/ruby-china/guides">https://github.com/ruby-china/guides</a>。</p>
      </div>
    </div>
  </div>

  <hr class="hide" />
  <div id="footer">
    <div class="wrapper">
      <p>本著作采用 <a href="https://creativecommons.org/licenses/by-sa/4.0/">创作共用 署名-相同方式共享 4.0 国际</a> 授权</p>
<p>“Rails”，“Ruby on Rails”，以及 Rails Logo 为 David Heinemeier Hansson 的商标。版权所有</p>

    </div>
  </div>

  <script type="text/javascript" src="javascripts/jquery.min.js"></script>
  <script type="text/javascript" src="javascripts/responsive-tables.js"></script>
  <script type="text/javascript" src="javascripts/guides.js"></script>
  <script type="text/javascript" src="javascripts/syntaxhighlighter.js"></script>
  <script type="text/javascript">
    syntaxhighlighterConfig = {
      autoLinks: false,
    };
    $(guidesIndex.bind);
  </script>
</body>
</html>
